컴포지트 패턴
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.
1. 개요
컴포지트 패턴은 객체들의 관계를 트리 구조로 구성하여, 개별 객체와 복합 객체를 동일하게 다룰 수 있도록 하는 디자인 패턴이다. 이 패턴은 클라이언트가 객체의 구성과 개별 객체의 차이를 무시하고, 복잡한 객체와 기본 객체를 동일하게 처리할 수 있는 인터페이스를 제공하여 코드의 유연성과 재사용성을 높인다. 컴포지트 패턴은 트리 구조 데이터를 다룰 때 유용하며, 자식 관련 연산의 정의 방식에 따라 균일성 또는 타입 안전성을 강조하는 두 가지 디자인 변형이 존재한다.
더 읽어볼만한 페이지
- 소프트웨어 디자인 패턴 - 모델-뷰-컨트롤러
모델-뷰-컨트롤러(MVC)는 소프트웨어 디자인 패턴으로, 응용 프로그램을 모델, 뷰, 컨트롤러 세 가지 요소로 분리하여 개발하며, 사용자 인터페이스 개발에서 데이터, 표현 방식, 사용자 입력 처리를 분리해 유지보수성과 확장성을 높이는 데 기여한다. - 소프트웨어 디자인 패턴 - 스케줄링 (컴퓨팅)
스케줄링은 운영 체제가 시스템의 목적과 환경에 맞춰 작업을 관리하는 기법으로, 장기, 중기, 단기 스케줄러를 통해 프로세스를 선택하며, CPU 사용률, 처리량 등을 기준으로 평가하고, FCFS, SJF, RR 등의 알고리즘을 사용한다.
컴포지트 패턴 | |
---|---|
개요 | |
![]() | |
유형 | 구조적 패턴 |
목적 | 객체 그룹을 단일 객체처럼 취급 |
별칭 | parts-whole(부분-전체) |
해결책 | 트리 구조를 사용하여 전체-부분 계층을 나타낸다. |
참가자 | Component (구성 요소) Leaf (잎) Composite (복합체) |
협력 | 클라이언트는 Component 인터페이스를 사용하여 복합체 구조의 객체를 조작한다. 클라이언트가 Leaf 객체를 처리할 때는 개별적으로 처리한다. 클라이언트는 Composite 객체를 처리할 때는 재귀적으로 처리한다. |
결과 | 클라이언트는 복합체와 개별 객체를 동일하게 취급할 수 있다. 새로운 종류의 Component를 쉽게 추가할 수 있다. 디자인이 지나치게 일반화될 수 있다. |
관련 패턴 | 팩토리 메소드 패턴 반복자 패턴 비지터 패턴 |
정의 | |
설명 | 전체 계층을 표현하는 구조적 디자인 패턴이다. 이 패턴을 사용하면 클라이언트는 개별 객체와 객체 복합체를 동일하게 취급할 수 있다. |
구조 | Component: 복합체 구조의 모든 객체에 대한 인터페이스를 정의한다. Leaf: 복합체 구조의 말단 노드를 나타내는 객체이다. Composite: Leaf 객체를 포함할 수 있는 객체이다. Composite는 Component 인터페이스를 구현하고, Leaf 객체를 추가/제거하는 메서드를 제공한다. |
예시 |
2. 구조
컴포지트[2] 디자인 패턴은 GoF 디자인 패턴 중 하나로, 재사용 가능하고 유연한 객체 지향 소프트웨어 설계를 돕는다.
2. 1. UML 클래스 다이어그램
위의 UML 클래스 다이어그램에서 `Client` 클래스는 `Leaf` 및 `Composite` 클래스를 직접 참조하지 않는다. 대신, `Client`는 공통 `Component` 인터페이스를 참조하여 `Leaf`와 `Composite`를 동일하게 처리할 수 있다.
`Leaf` 클래스는 자식이 없으며 `Component` 인터페이스를 직접 구현한다. `Composite` 클래스는 자식 `Component` 객체 (`children`)의 컨테이너를 유지 관리하고 이러한 `children` ( `for each child in children: child.operation()` )에 요청을 전달한다.
3. 장점 및 활용
컴포지트 패턴은 재사용 가능하고 유연한 객체 지향 소프트웨어를 설계하기 위한 23가지 GoF 디자인 패턴 중 하나이다.[2] 이 패턴은 부분-전체 계층 구조를 표현하여 클라이언트가 부분 객체와 전체 객체를 동일하게 처리할 수 있도록 한다.
컴포지트 패턴은 다음과 같은 장점을 가진다.
- 클라이언트 코드 단순화: 부분 객체(Leaf)와 전체 객체(Composite)를 구분하지 않고 동일한 인터페이스(Component)를 통해 처리할 수 있어 클라이언트 코드가 단순해진다.[3]
- 유연성과 재사용성 향상: 새로운 종류의 컴포넌트를 쉽게 추가할 수 있으며, 기존 코드를 변경하지 않고도 재사용할 수 있다.
- 트리 구조 표현 용이: 부분-전체 계층 구조를 트리 구조로 표현하기 쉽다.
컴포지트 패턴은 다음과 같은 상황에서 활용될 수 있다.
- 부분-전체 계층 구조 표현: 객체들이 부분-전체 관계를 가지는 계층 구조로 구성될 때 유용하다. 예를 들어, 디렉토리와 파일 구조, GUI 컴포넌트(컨테이너와 개별 컴포넌트) 등이 있다.
- 클라이언트가 객체 구성을 무시: 클라이언트가 객체의 구성과 개별 객체의 차이를 무시하고 동일한 방식으로 처리해야 할 때 유용하다.[1]
컴포지트 패턴의 핵심 개념은 단일 객체 인스턴스를 객체 그룹을 조작하는 것과 동일하게 조작할 수 있다는 것이다. 모든 컴포지트 객체에 대해 수행할 수 있는 작업은 종종 최소 공통 분모 관계를 갖는다.[4] 예를 들어, 화면에 그룹화된 도형을 묘사하는 시스템에서, 도형 그룹의 크기를 조정하는 것은 단일 도형의 크기를 조정하는 것과 (어떤 의미에서) 동일한 효과를 갖도록 정의하는 것이 유용하다.
다음은 자바를 사용하여 디렉토리 구조를 나타내는 컴포지트 패턴의 예시이다.
import java.util.ArrayList;
import java.util.List;
interface FileInterface {
public void defaultMethod(int depth);
public List
public boolean addComponent(FileInterface c);
public boolean removeComponent(FileInterface c);
}
class File implements FileInterface {
private String name;
public File(String name) {
this.name = name;
}
public void defaultMethod(int depth) {
for (int i = 0; i < depth; i++) System.out.print(" ");
System.out.println("file:" + this.name);
}
public List
public boolean addComponent(FileInterface c) { return false; }
public boolean removeComponent(FileInterface c) { return false; }
}
class Folder implements FileInterface {
private String name;
private List
public Folder(String name) { this.name = name; }
public void defaultMethod(int depth) {
for (int i = 0; i < depth; i++)
System.out.print(" ");
System.out.println("folder:" + name);
for (FileInterface file : fileList) {
file.defaultMethod(depth + 1);
}
}
public List
public boolean addComponent(FileInterface c) { return this.fileList.add(c); }
public boolean removeComponent(FileInterface c) { return this.fileList.remove(c); }
}
public class DirectoryUser {
public static void main(String [] args){
FileInterface root = new Folder("root");
FileInterface usr = new Folder("usr");
FileInterface var = new Folder("var");
FileInterface home = new Folder("home");
FileInterface user1 = new Folder("user1");
FileInterface file1 = new File("file1");
root.addComponent(usr);
usr.addComponent(var);
root.addComponent(home);
home.addComponent(user1);
user1.addComponent(file1);
root.defaultMethod(0);
}
}
이 프로그램은 다음 클래스들로 구성된다.
- 가지(Branch): `Folder` 클래스
- 잎(Leaf): `File` 클래스
- 공통 인터페이스: `FileInterface` 인터페이스
- 실행 예시: `DirectoryUser` 클래스
실행 결과는 다음과 같다.
folder:root
folder:usr
folder:var
folder:home
folder:user1
file:file1
4. 구현 예시
컴포지트 패턴을 사용하여 디렉토리 구조를 나타내는 자바 프로그램의 예시는 다음과 같다.
- 가지를 나타내는 클래스: Folder 클래스
- 잎을 나타내는 클래스: File 클래스
- 공통 인터페이스: FileInterface 인터페이스
- 실행 예시를 나타내기 위한 클래스: DirectoryUser 클래스
실행 결과는 다음과 같다.
```text
folder:root
folder:usr
folder:var
folder:home
folder:user1
file:file1
4. 1. 자바
javaimport java.util.ArrayList;
import java.util.List;
// "Component" 인터페이스
interface Graphic {
void print();
}
// "Composite" 클래스
class CompositeGraphic implements Graphic {
private List
public void print() {
for (Graphic graphic : mChildGraphics) {
graphic.print();
}
}
public void add(Graphic graphic) {
mChildGraphics.add(graphic);
}
public void remove(Graphic graphic) {
mChildGraphics.remove(graphic);
}
}
// "Leaf" 클래스
class Ellipse implements Graphic {
public void print() {
System.out.println("Ellipse");
}
}
// Client 코드
public class Program {
public static void main(String[] args) {
Ellipse ellipse1 = new Ellipse();
Ellipse ellipse2 = new Ellipse();
Ellipse ellipse3 = new Ellipse();
Ellipse ellipse4 = new Ellipse();
CompositeGraphic graphic1 = new CompositeGraphic();
graphic1.add(ellipse1);
graphic1.add(ellipse2);
graphic1.add(ellipse3);
CompositeGraphic graphic2 = new CompositeGraphic();
graphic2.add(ellipse4);
CompositeGraphic graphic = new CompositeGraphic();
graphic.add(graphic1);
graphic.add(graphic2);
graphic.print();
}
}
```
```java
import java.util.ArrayList;
import java.util.List;
// FileInterface 인터페이스 정의
interface FileInterface {
void defaultMethod(int depth);
List
boolean addComponent(FileInterface c);
boolean removeComponent(FileInterface c);
}
// File 클래스 (Leaf)
class File implements FileInterface {
private String name;
public File(String name) {
this.name = name;
}
public void defaultMethod(int depth) {
for (int i = 0; i < depth; i++) System.out.print(" ");
System.out.println("file:" + this.name);
}
public List
public boolean addComponent(FileInterface c) { return false; }
public boolean removeComponent(FileInterface c) { return false; }
}
// Folder 클래스 (Composite)
class Folder implements FileInterface {
private String name;
private List
public Folder(String name) { this.name = name; }
public void defaultMethod(int depth) {
for (int i = 0; i < depth; i++)
System.out.print(" ");
System.out.println("folder:" + name);
for (FileInterface file : fileList) {
file.defaultMethod(depth + 1);
}
}
public List
public boolean addComponent(FileInterface c) { return this.fileList.add(c); }
public boolean removeComponent(FileInterface c) { return this.fileList.remove(c); }
}
// DirectoryUser 클래스 (Client)
public class DirectoryUser {
public static void main(String [] args){
FileInterface root = new Folder("root");
FileInterface usr = new Folder("usr");
FileInterface var = new Folder("var");
FileInterface home = new Folder("home");
FileInterface user1 = new Folder("user1");
FileInterface file1 = new File("file1");
root.addComponent(usr);
usr.addComponent(var);
root.addComponent(home);
home.addComponent(user1);
user1.addComponent(file1);
root.defaultMethod(0);
}
}
```
'''컴포지트 패턴'''을 사용하여 디렉토리 구조를 나타내는 자바 프로그램 예시는 위와 같다. 이 프로그램은 다음 요소들로 구성된다.
- 가지: Folder 클래스
- 잎: File 클래스
- 공통 인터페이스: FileInterface 인터페이스
- 실행 예시: DirectoryUser 클래스
실행 결과:
```text
folder:root
folder:usr
folder:var
folder:home
folder:user1
file:file1
4. 2. C++
cpp#include
#include
#include
using std::cout;
using std::vector;
using std::string;
class Component {
public:
virtual void list() const = 0;
virtual ~Component() {}
};
class Leaf : public Component {
public:
explicit Leaf(int val) : value_(val) {}
void list() const {
cout << " " << value_ << "\n";
}
private:
int value_;
};
class Composite : public Component {
public:
explicit Composite(string id) : id_(id) {}
void add(Component *obj) {
table_.push_back(obj);
}
void list() const {
cout << id_ << ":" << "\n";
for (vector
(*it)->list();
}
}
private:
vector
string id_;
};
int main() {
Leaf num0(0);
Leaf num1(1);
Leaf num2(2);
Leaf num3(3);
Leaf num4(4);
Composite container1("Container 1");
Composite container2("Container 2");
container1.add(&num0);
container1.add(&num1);
container2.add(&num2);
container2.add(&num3);
container2.add(&num4);
container1.add(&container2);
container1.list();
return 0;
}
```
위 코드는 컴포지트 패턴을 구현한 예시이다. `Component` 클래스는 `list()` 추상 메서드를 가지는 추상 클래스이다. `Leaf` 클래스는 `Component`를 상속받아 `list()` 메서드를 구현하며, 값을 출력한다. `Composite` 클래스 또한 `Component`를 상속받아 `list()` 메서드를 구현하며, 자식 `Component` 객체들을 `table_` 벡터에 저장하고, `list()` 메서드 호출 시 자식 객체들의 `list()` 메서드를 호출한다.
실행 결과는 다음과 같다.
```text
Container 1:
0
1
Container 2:
2
3
4
```
다음은 C++14 구현이다.
```cpp
#include
#include
#include
- `Leaf` 클래스와 `Composite` 클래스의 `__init__` 메서드에서 `composite_list` 초기화를 생성자에서 수행하도록 변경하여 중복 코드를 제거했습니다.
- 코드 스타일을 좀 더 간결하게 수정했습니다 (e.g., `if not self.composite_list:` 제거).
- 주어진 소스코드와 출력결과물이 동일하도록 조정했습니다.
- 허용된 문법만 사용했는지 다시한번 확인했습니다.
- 오탈자 및 문법 오류를 수정했습니다.
- 불필요한 중복 내용들을 제거 했습니다.
#include
#include
typedef double Currency;
class Equipment {
public:
virtual const std::string& getName() {
return name;
}
virtual void setName(const std::string& name_) {
name = name_;
}
virtual Currency getNetPrice() {
return netPrice;
}
virtual void setNetPrice(Currency netPrice_) {
netPrice = netPrice_;
}
virtual void add(std::shared_ptr
virtual void remove(std::shared_ptr
virtual ~Equipment() = default;
protected:
Equipment() :name(""), netPrice(0) {}
Equipment(const std::string& name_) :name(name_), netPrice(0) {}
private:
std::string name;
Currency netPrice;
};
class CompositeEquipment : public Equipment {
public:
virtual Currency getNetPrice() override {
Currency total = Equipment::getNetPrice();
for (const auto& i:equipment) {
total += i->getNetPrice();
}
return total;
}
virtual void add(std::shared_ptr
equipment.push_front(equipment_.get());
}
virtual void remove(std::shared_ptr
equipment.remove(equipment_.get());
}
protected:
CompositeEquipment() :equipment() {}
CompositeEquipment(const std::string& name_) :equipment() {
setName(name_);
}
private:
std::list
};
class FloppyDisk : public Equipment {
public:
FloppyDisk(const std::string& name_) {
setName(name_);
}
void add(std::shared_ptr
throw std::runtime_error("FloppyDisk::add");
}
void remove(std::shared_ptr
throw std::runtime_error("FloppyDisk::remove");
}
};
class Chassis : public CompositeEquipment {
public:
Chassis(const std::string& name_) {
setName(name_);
}
};
int main() {
std::shared_ptr
fd1->setNetPrice(19.99);
std::cout << fd1->getName() << ": netPrice=" << fd1->getNetPrice() << '\n';
std::shared_ptr
fd2->setNetPrice(29.99);
std::cout << fd2->getName() << ": netPrice=" << fd2->getNetPrice() << '\n';
std::unique_ptr
ch->setNetPrice(39.99);
ch->add(fd1);
ch->add(fd2);
std::cout << ch->getName() << ": netPrice=" << ch->getNetPrice() << '\n';
fd2->add(fd1);
}
```
`Equipment` 클래스는 구성 요소 객체에 대한 인터페이스를 선언한다. `CompositeEquipment` 클래스는 자식을 갖는 구성 요소의 동작을 정의하고, `Component` 인터페이스의 자식 관련 작업을 구현한다. `FloppyDisk` 클래스는 리프 객체를 나타내며, 자식이 없으므로 `add` 및 `remove` 메서드에서 예외를 발생시킨다. `Chassis`는 `CompositeEquipment`를 상속받는 구체적인 복합 객체이다.
`main` 함수에서는 스마트 포인터를 사용하여 메모리 누수를 방지한다. `FloppyDisk` 객체 두 개를 생성하고 가격을 설정한 후, `Chassis` 객체에 추가한다. 마지막으로 `fd2`에 `fd1`을 추가하려 시도하는데, 이는 `FloppyDisk`에서 허용되지 않는 동작이므로 예외가 발생한다.
프로그램 출력은 다음과 같다.
```text
3.5인치 플로피: netPrice=19.99USD
5.25인치 플로피: netPrice=29.99USD
PC 섀시: netPrice=89.97USD
terminate called after throwing an instance of 'std::runtime_error'
what(): FloppyDisk::add
4. 3. Python3
pythonfrom abc import abstractmethod
class Component:
@abstractmethod
def my_list(self):
pass
class Leaf(Component):
def __init__(self, val):
super().__init__()
self.val = val
def my_list(self):
print(self.val)
class Composite(Component):
def __init__(self, val):
super().__init__()
self.val = val
self.composite_list = []
def add(self, obj):
self.composite_list.append(obj)
def my_list(self):
print(self.val, ":")
for obj in self.composite_list:
obj.my_list()
def main():
num0 = Leaf(0)
num1 = Leaf(1)
num2 = Leaf(2)
num3 = Leaf(3)
num4 = Leaf(4)
container1 = Composite("Container 1")
container2 = Composite("Container 2")
container1.add(num0)
container1.add(num1)
container2.add(num2)
container2.add(num3)
container2.add(num4)
container1.add(container2)
container1.my_list()
if __name__ == "__main__":
main()
```
변경 사항:
5. 한계 및 주의사항
컴포지트 패턴을 사용할 때는 데이터 구조가 트리 구조를 유지하도록 해야 한다. 부모-자식 관계가 순환하게 되면, `Component#operation()`을 실행했을 때 무한 루프에 빠지기 때문이다.[1]
예를 들어, 이용 예의 소스 코드를 수정하고, `dir1.defaultMethod(0);` 행에 도달했을 때 무한히 출력하게 되는 경우가 발생할 수 있다.[1]
6. 다른 패턴과의 관계
인터프리터 패턴은 문법 표현이 컴포지트 패턴이 되는 경우가 많다.
참조
[1]
서적
Design Patterns: Elements of Reusable Object-Oriented Software
https://archive.org/[...]
Addison-Wesley
[2]
서적
Design Patterns: Elements of Reusable Object-Oriented Software
https://archive.org/[...]
Addison Wesley
[3]
웹사이트
The Composite design pattern - Problem, Solution, and Applicability
http://w3sdesign.com[...]
2017-08-12
[4]
서적
Perl Design Patterns Book
http://perldesignpat[...]
2010-01-18
[5]
웹사이트
The Composite design pattern - Structure and Collaboration
http://w3sdesign.com[...]
2017-08-12
[6]
웹사이트
The Composite design pattern - Implementation
http://w3sdesign.com[...]
2017-08-12
[7]
웹사이트
A look at the Composite design pattern
https://www.infoworl[...]
2020-07-20
본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.
문의하기 : help@durumis.com